﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using log4net;
using Sherwood.Configuration;
using Sherwood.Security;
using System.Net;
using Sherwood.SignOn.Client.Session;
using Sherwood.Caching;
using System.Web.Caching;

namespace Sherwood.SignOn.Client
{
    /// <summary>
    /// Example implementation of an SSO client.
    /// </summary>
    public class SignOnClient
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(SignOnClient));

        private clientSettings _settings = null;
        private ISessionRepository _sessionRepository = null;



        /// <summary>
        /// Creates a new instance of the SSO client.
        /// </summary>
        public SignOnClient()
        {

        }

        /// <summary>
        /// Returns an instance of a persistent session repository which
        /// can be used to maintain records of the client's active sessions.
        /// </summary>
        public virtual ISessionRepository SessionRepository
        {
            get
            {
                if (_sessionRepository == null)
                {
                    _sessionRepository = new SqlSessionRepository(Settings.sessionRepository.connectionString, Settings.client.code, Settings.client.code, (int)Settings.sessionRepository.sessionTimeoutInMinutes);
                }
                return _sessionRepository;
            }
            set
            {
                _sessionRepository = value;
            }
        }


        private string _configFile = null;
        /// <summary>
        /// Path to the configuration file for sign on client
        /// </summary>
        public string ConfigurationFilePath
        {
            get
            {
                if (_configFile == null)
                {
                    _configFile = System.Configuration.ConfigurationManager.AppSettings["SignOnClientConfigurationFile"];
                    if (!string.IsNullOrEmpty(_configFile) && !System.IO.File.Exists(_configFile) && HttpContext.Current != null && HttpContext.Current.Server != null)
                    {
                        _configFile = HttpContext.Current.Server.MapPath(_configFile);
                    }
                }
                return _configFile;
            }
        }

        public clientSettings Settings
        {
            get
            {
                if (_settings == null)
                {
                    if (!string.IsNullOrEmpty(ConfigurationFilePath))
                    {
                        _settings = clientSettings.Load(ConfigurationFilePath);
                    }
                    else
                    {
                        return null;
                    }
                }
                return _settings;
            }
        }


        /// <summary>
        /// Shortcut to retreiving SsoClientCode from config.
        /// </summary>
        protected virtual string ClientCode
        {
            get
            {
                return Settings.client.code ?? "";
            }
        }


        #region User/sesion info


        
        /// <summary>
        /// Sets SessionId, UserAccountId, IsAnonymousUser and SignOnSessionId item variables if a session exists
        /// otherwise, if no session exists, these variables are cleared.
        /// </summary>
        /// <returns>Current local sessionId or null if no session exists</returns>
        private string CheckSession()
        {
            if (HttpContext.Current.Items["SessionId"] == null)
            {
                string userAccountId = null;
                string sessionId = null;
                string signOnSessionId = null;
                string sessionCookieValue = Cookie.GetCookieValue(SessionCookieName);
                bool isAnonymousUser = false;
                bool isPersistentSession = true; //assume the "worst"
                if (!string.IsNullOrEmpty(sessionCookieValue))
                {
                    string[] parts = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(sessionCookieValue)).Split('|');
                    if (parts.Length >= 3)
                    {
                        sessionId = parts[0];
                        if (!SessionRepository.SessionActive(sessionId))
                        {
                            sessionId = null;
                            Cookie.RemoveCookie(SessionCookieName);
                        }
                        else
                        {
                            signOnSessionId = parts[1];
                            userAccountId = parts[2];
                            if (parts.Length >= 4)
                                isPersistentSession = Convert.ToBoolean(parts[3]);
                            //Ensure that useraccountid is set to null for annonymous signins
                            try
                            {
                                Guid userAccountIDGuid = new Guid(userAccountId);
                                if (userAccountIDGuid.Equals(Guid.Empty))
                                {
                                    isAnonymousUser = true;
                                    userAccountId = null;
                                }
                            }
                            catch
                            {
                                isAnonymousUser = true;
                                userAccountId = null;
                            }
                        }
                    }
                }
                SignOnSessionId = signOnSessionId;
                UserAccountId = userAccountId;
                IsAnonymousUser = isAnonymousUser;
                IsPersistentSession = isPersistentSession;
                HttpContext.Current.Items["SessionId"] = sessionId;
                return sessionId;
            }
            else
            {
                return HttpContext.Current.Items["SessionId"] as string;
            }
        }


        /// <summary>
        /// Returns the current (local) session id or null if no session exists.
        /// Internally sets _userAccountId
        /// </summary>
        public string SessionId
        {
            get
            {
                return CheckSession();
            }
        }

        /// <summary>
        /// Returns the SessionId provided by the sign on server (global session Id)
        /// </summary>
        public string SignOnSessionId
        {
            
            get
            {
                CheckSession();
                return HttpContext.Current.Items["SignOnSessionId"] as string;
            }
            set
            {
                HttpContext.Current.Items["SignOnSessionId"] = value;
            }
        }

        /// <summary>
        /// Returns the current user's user accountId (or  null if hte user is not signed in)
        /// </summary>
        public string UserAccountId
        {

            get
            {
                CheckSession();
                return HttpContext.Current.Items["UserAccountId"] as string;
            }
            set
            {
                HttpContext.Current.Items["UserAccountId"] = value;
            }
        }

        /// <summary>
        /// Indicates that the current session is a "persistent" session, meaning it may have existed beyond the lifetime of a single browser session.
        /// </summary>
        public bool IsPersistentSession
        {

            get
            {
                CheckSession();
                return HttpContext.Current.Items["IsPersistentSession"] == null ? true : (bool)HttpContext.Current.Items["IsPersistentSession"];
            }
            set
            {
                HttpContext.Current.Items["IsPersistentSession"] = value;
            }
        }

        /// <summary>
        /// Indicates that the currently signed on user is anonymous (not linked to a user account)
        /// </summary>
        public bool IsAnonymousUser
        {
            get
            {
                CheckSession();
                return HttpContext.Current.Items["IsAnonymousUser"] == null ? true : (bool)HttpContext.Current.Items["IsAnonymousUser"];
            }
            set
            {
                HttpContext.Current.Items["IsAnonymousUser"] = value;
            }
        }

        /// <summary>
        /// Indicates whether or not the user is signed in.
        /// </summary>
        public bool IsUserSignedIn
        {
            get
            {
                return SessionId != null && UserProfile != null;
            }
        }


        /// <summary>
        /// Returns the username of the current user (if user this information can be retreived)
        /// Otherwise returns an empty string
        /// </summary>
        public string Username
        {
            get
            {
                if (UserProfile != null)
                    return UserProfile.username ?? string.Empty;
                return string.Empty;
            }
        }




        /// <summary>
        /// Returns the UserProfile object for this user.
        /// This functionality is only applicable to "trusted clients"
        /// for which the SSO Server has a record of their RSA public key.
        /// </summary>
        public virtual getUserProfileResponse UserProfile
        {
            get
            {

                string signOnSessionId = SignOnSessionId;
                if (signOnSessionId != null)
                {
                    var profile = GetCacheValue("SsoUserProfile" + signOnSessionId) as getUserProfileResponse;
                    if (profile == null) //If profile doesn't exist in session
                    {
                        if (IsAnonymousUser)
                        {
                            profile = new getUserProfileResponse();
                            profile.username = "";
                            profile.firstName = "Anonymous";
                            profile.lastName = "User";
                            profile.attribute = null;
                            profile.role = null;
                            profile.culture = null;
                            profile.userAccountId = "";
                            profile.mobile = "";
                            profile.gender = 0;
                            profile.dateOfBirth = DateTime.Today;
                            profile.country = "";
                            profile.email = "";
                        }
                        else
                        {
                            //AttempIsPersistentSessionle information from session
                            var servicesClient = new SignOnServicesClient(Settings.server.baseUrl,
                                                                          Settings.client.code,
                                                                          Settings.client.rsaPrivateKey);
                            Guid signOnSessionIdGuid = new Guid(signOnSessionId);
                            try
                            {
                                profile = servicesClient.GetUserProfile(signOnSessionIdGuid);
                            }
                            catch //error occured retreiving profile from server
                            {
                                InternalSignOut();
                                return null;
                            }
                        }
                        //if (profile == null)
                        //    return new getUserProfileResponse();
                        SetCacheValue("SsoUserProfile" + signOnSessionId, profile, new TimeSpan(0, 30, 0));
                        if (profile != null)
                        {
                            SessionRepository.StoreProfile(SessionId, profile.ToString());
                        }
                        else
                        {
                            if (SessionId != null)
                            {
                                SessionRepository.StoreProfile(SessionId, "");
                            }
                        }
                    }
                    return profile;
                }
                return null; // new getUserProfileResponse();
            }
        }


        /// <summary>
        /// Returns the Roles of the current user.
        /// This functionality is only applicable to clients who have access
        /// to a role provider service.
        /// </summary>
        public virtual IEnumerable<string> UserRoles
        {
            get
            {
                var roles = HttpContext.Current.Items["SsoUserRoles"] as List<string>;
                if (roles != null) 
                    return roles;

                roles = new List<string>();
                var profile = UserProfile;
                if (profile != null && profile.role != null)
                {
                    foreach (var role in profile.role)
                    {
                        string roleName = role.source + ":" + role.name;
                        if (!roles.Contains(roleName))
                        {
                            roles.Add(roleName);
                        }
                    }
                }
                HttpContext.Current.Items["SsoUserRoles"] = roles;
                return roles;
            }
        }

        /// <summary>
        /// Returns the Attributes of the current user.
        /// </summary>
        public virtual IDictionary<string, string> Attributes
        {
            get
            {
                var attributes = HttpContext.Current.Items["SsoUserAttributes"] as Dictionary<string, string>;
                if (attributes != null)
                    return attributes;

                attributes = new Dictionary<string, string>();
                var profile = UserProfile;
                if (profile != null && profile.attribute != null)
                {
                    foreach (var attribute in profile.attribute)
                    {
                        string key = attribute.source + ":" + attribute.name;
                        if (!attributes.ContainsKey(key))
                            attributes.Add(key, attribute.value);
                    }
                }
                HttpContext.Current.Items["SsoUserAttributes"] = attributes;
                return attributes;
            }
        }


        #endregion


        #region Session methods

        private static Dictionary<string, object> _dummyCache;


        /// <summary>
        /// Gets session variable. If no HttpContext exists, session is simulated using a static (application wide) variable.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public object GetCacheValue(string name)
        {

            if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Cache != null)
            {

                return System.Web.HttpContext.Current.Cache[name];
            }
            else
            {
                if (_dummyCache == null)
                {
                    _dummyCache = new Dictionary<string, object>();
                }
                if (_dummyCache.ContainsKey(name))
                {
                    return _dummyCache[name];
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Sets cache variable. If no HttpContext exists, session is simulated using a static (application wide) variable.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public void SetCacheValue(string name, object value, TimeSpan slidingTimeout)
        {
            if (HttpContext.Current != null && HttpContext.Current.Cache != null)
            {
                if (HttpContext.Current.Cache[name] == null && value != null)
                {
                    HttpContext.Current.Cache.Add(name, value, null, Cache.NoAbsoluteExpiration, slidingTimeout, CacheItemPriority.Normal, null);
                }
                else
                {
                    if (value != null)
                    {
                        HttpContext.Current.Cache[name] = value;
                    }
                    else
                    {
                        HttpContext.Current.Cache.Remove(name);
                    }
                }
            }
            else
            {
                if (_dummyCache == null)
                {
                    _dummyCache = new Dictionary<string, object>();
                }
                if (value != null)
                {
                    _dummyCache[name] = value;
                }
                else
                {
                    if (_dummyCache.ContainsKey(name))
                    {
                        _dummyCache.Remove(name);
                    }
                }
            }
        }
        
        #endregion


        /// <summary>
        /// Name of session cookie
        /// </summary>
        protected virtual string SessionCookieName
        {
            get
            {
                return ClientCode + "SsoSessionId";
            }
        }

     

        #region Sign in / sign out


        /// <summary>
        /// Registers a local session and redirects the user to the SSO server 
        /// </summary>
        /// <returns>Sign in request url</returns>
        public virtual string RedirectToSsoServer()
        {
            string currentUrl = GetCurrentUrl();
            return RedirectToSsoServer(currentUrl,true, false);
        }

        /// <summary>
        /// Registers a local session and redirects the user to the SSO server 
        /// </summary>
        /// <returns>Sign in request url</returns>
        public virtual string RedirectToSsoServer(bool requireLogin)
        {
            string currentUrl = GetCurrentUrl();
            return RedirectToSsoServer(currentUrl, requireLogin, false);
        }

        /// <summary>
        /// Registers a local session and redirects the user to the SSO server 
        /// </summary>
        /// <returns>Sign in request url</returns>
        public virtual string RedirectToSsoServer(bool requireLogin, bool requireNonPersistentSession)
        {
            string currentUrl = GetCurrentUrl();
            return RedirectToSsoServer(currentUrl, requireLogin, requireNonPersistentSession);
        }

        /// <summary>
        /// Registers a local session and redirects the user to the SSO server 
        /// </summary>
        /// <returns>Sign in request url</returns>
        public virtual string RedirectToSsoServer(string currentUrl, bool requireLogin)
        {
            return RedirectToSsoServer(currentUrl, requireLogin, false);
        }

        public virtual string RedirectToSsoServer(string currentUrl, bool requireLogin, bool requireNonPersistentSession)
        {
            //Perform local sign out if user is logged in
            string sessionId = SessionId;
            if (sessionId != null)
            {
                
                SessionRepository.DeleteSession(sessionId);
                Cookie.RemoveCookie(SessionCookieName);
            }

            //Register a new session in session repository (but don't set cookie -- this should only be done after sign in)
            string newSessionId = SessionRepository.CreateSession();

            string clientSessionId = HttpUtility.UrlEncode(newSessionId);
            string encodedUrl = HttpUtility.UrlEncode(currentUrl);
            string rawSignInUrl = Settings.client.signInUrl;
            string rawSignOutUrl = Settings.client.signOutUrl;
            string returnUrl = HttpUtility.UrlEncode(GetResourceUrl(rawSignInUrl).Replace("[CurrentUrl]", currentUrl).Replace("[UrlEncodedCurrentUrl]", encodedUrl));
            string signOutUrl = HttpUtility.UrlEncode(GetResourceUrl(rawSignOutUrl));
            string signInUrl = Settings.server.baseUrl.Trim().TrimEnd('/') + "/SignIn/?ClientSessionId=" + clientSessionId + "&SignOutUrl=" + signOutUrl + "&ReturnUrl=" + returnUrl;
            if (!requireLogin)
            {
                signInUrl += "&RequireLogin=False";
            }
            if (requireNonPersistentSession)
            {
                signInUrl += "&RequireNonPersistentSession=True";
            }
            if (HttpContext.Current != null && HttpContext.Current.Response != null)
            {
                HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                HttpContext.Current.Response.Redirect(signInUrl);
            }
            return signInUrl;
        }

        private string GetResourceUrl(string configuredUrl)
        {
            if (configuredUrl.StartsWith("~") && HttpContext.Current != null)
                return HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) +
                                HttpContext.Current.Request.ApplicationPath.TrimEnd('/') + configuredUrl.Substring(1);
            return configuredUrl.Replace("[Host]", GetCurrentHost()).Replace("[ProtocolAndHost]", GetCurrentHostAndProtocol());
        }


        /// <summary>
        /// Performs local sign-in of the user.
        /// </summary>
        /// <param name="ticket">Ticket from SSO server (base64, utf8 encoded string)</param>
        /// <param name="signature">Ticket signature</param>
        /// <returns>True if sign on was successful, otherwise false</returns>
        public virtual bool ClientSignOn(string ticket, string signature)
        {
            if (!string.IsNullOrEmpty(ticket) && !string.IsNullOrEmpty(signature))
            {
                //Decode ticket (expected to be base64 encoded string of UTF8 encoded bytes)
                ticket = Encoding.UTF8.GetString(Convert.FromBase64String(ticket));

                try
                {
                    //Verify sender
                    Rsa rsa = Rsa.FromPublicKey(Settings.server.rsaPublicKey);
                    if (!rsa.SHA1Verify(ticket, signature))
                    {
                        throw new Exception("Ticket verification failed.");
                    }
                    //Parse ticket contents
                    string[] ticketParts = ticket.Split('|');
                    if (ticketParts.Length >= 6)
                    {
                        string prefix = ticketParts[0];
                        string ssoSessionId = ticketParts[1];
                        string clientSessionId = ticketParts[2];
                        string ipAddress = ticketParts[3];
                        string userAccountId = ticketParts[4];
                        string timestampString = ticketParts[5];
                        bool isPersistentSession = true; //assume the worst
                        if (ticketParts.Length >= 7)
                            isPersistentSession = Convert.ToBoolean(ticketParts[6]);
                        DateTime timestamp = DateTime.Parse(timestampString);

                        //Validate ticket contents

                        if (!prefix.ToUpper().Equals("SI"))
                            throw new Exception("Ticket is not a sign on ticket.");
                        //Not very useful checking ip address at this point because ip address seen by server may be different to that seen by client
                        //because of different network configurations (e.g. server may be on local domain and client on a remote server).
                        //if (!GetUserIpAddress().Equals(ipAddress))
                        //    throw new Exception("Request to sign on server made from a different ip address (" + ipAddress + "), than current ip address (" + GetUserIpAddress() + ").");
                        if (string.IsNullOrEmpty(ssoSessionId))
                            return false; // throw new Exception("Serversessionid is null or empty.");
                        if (string.IsNullOrEmpty(clientSessionId))
                            return false; // throw new Exception("Clientsessionid is null or empty.");
                        if (Math.Abs((DateTime.Now - timestamp).TotalSeconds) >
                            (int) Settings.server.ticketValidityInSeconds)
                            //ticket timestamp not within the allowed timeframe. The absolute time difference is considered because the time on the client machine might not be synchronized with the time on the sign on server (i.e. need to allow for client to be ahead).
                            return false;
                        // throw new Exception("Ticket timestamp did not corrospond with the current time.");
                        if (!SessionRepository.ActivateSession(clientSessionId))
                        {
                            if (Log.IsDebugEnabled)
                                Log.Debug("Unable to activate sign on session.");
                            return false; //Session could not be activated locally
                        }

                        //Session has been activated
                        string cookieValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(clientSessionId + "|" +
                                                                                           ssoSessionId + "|" +
                                                                                           userAccountId + "|" +
                                                                                           isPersistentSession));
                        Cookie.SetCookie(SessionCookieName, cookieValue, GetCookieDomain());

                        return true; //User successfully signed in
                    }
                }
                catch (Exception ex)
                {
                    
                    throw new Exception("Local sign on failed.\n" + ex.Message);
                    return false;
                }
            }
            return false;
        }

        /// <summary>
        /// Returns the cookie domain from the configuration that best matches the current request
        /// </summary>
        /// <returns></returns>
        protected string GetCookieDomain()
        {
            string domain = "";
            string currentHost = GetCurrentHost().ToLower();
            string currentProtocolAndHost = GetCurrentHost().ToLower();
            if (Settings.client.cookieDomains != null)
            {
                foreach (var cookieDomain in  Settings.client.cookieDomains.cookieDomain)
                {
                    string hostPattern = cookieDomain.hostPattern.ToLower().Replace("[host]", currentHost).Replace("[protocolandhost]",currentProtocolAndHost);
                    if (Regex.IsMatch(currentHost,hostPattern))
                        return cookieDomain.cookieDomain.ToLower().Replace("[host]", currentHost).Replace("[protocolandhost]", currentProtocolAndHost);
                }
            }
            return "";
        }

        /// <summary>
        /// Deletes the local session.
        /// This method is indended for clearing signon sessions in case of problems retreiving 
        /// the user's current profile from the Sign-On server.
        /// </summary>
        /// <returns></returns>
        protected virtual void InternalSignOut()
        {
            if (SessionId != null)
            {
                //Complete local sign-out
                SessionRepository.DeleteSession(SessionId);
                Cookie.RemoveCookie(SessionCookieName);
            }
        }

        /// <summary>
        /// Performs a local sign-out.
        /// Session cookies are removed and the session is removed from the session repository.
        /// </summary>
        /// <param name="ticket"></param>
        /// <param name="signature"></param>
        /// <returns></returns>
        public virtual bool ClientSignOut(string ticket, string signature)
        {
            if (!string.IsNullOrEmpty(ticket) && !string.IsNullOrEmpty(signature))
            {
                //Decode ticket (expected to be base64 encoded string of UTF8 encoded bytes)
                ticket = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(ticket));

                try
                {
                    //Verify sender
                    Rsa rsa = Rsa.FromPublicKey(Settings.server.rsaPublicKey);
                    if (rsa.SHA1Verify(ticket, signature))
                    {
                        //Parse ticket contents
                        string[] ticketParts = ticket.Split('|');
                        if (ticketParts.Length >= 6)
                        {
                            string prefix = ticketParts[0];
                            string ssoSessionId = ticketParts[1];
                            string clientSessionId = ticketParts[2];
                            string ipAddress = ticketParts[3];
                            string userAccountId = ticketParts[4];
                            string timestampString = ticketParts[5];
                            DateTime timestamp = DateTime.MinValue;
                            try
                            {
                                timestamp = DateTime.Parse(timestampString);
                            }
                            catch
                            {
                                return false;
                            }

                            //Validate ticket contents
                            //Note: we can't check IP address, since sign out requests can be made by sign-out server (rather than the user)
                            if (!prefix.ToUpper().Equals("SO"))
                                return false; //Not a sign out ticket.
                            if (string.IsNullOrEmpty(ssoSessionId))
                                return false; //Invalid server sessionid
                            if (string.IsNullOrEmpty(clientSessionId))
                                return false; //Invalid sessionid
                            if (Math.Abs((DateTime.Now - timestamp).TotalSeconds) > (int)Settings.server.ticketValidityInSeconds)
                                return false; //ticket timestamp not within the allowed timeframe. The absolute time difference is considered because the time on the client machine might not be synchronized with the time on the sign on server (i.e. need to allow for client to be ahead).

                            //Complete local sign-out
                            SessionRepository.DeleteSession(clientSessionId);
                            Cookie.RemoveCookie(SessionCookieName);
                            return true;
                        }

                    }
                }
                catch
                {
                    return false;
                }
            }
            return false;
        }

        #endregion

        #region Utility methods

        protected string GetCurrentHost()
        {
            if (HttpContext.Current != null && HttpContext.Current.Request != null && HttpContext.Current.Request.Url != null)
            {
                return HttpContext.Current.Request.Url.Host;
            }
            return "";
        }

        protected string GetCurrentHostAndProtocol()
        {
            if (HttpContext.Current != null && HttpContext.Current.Request != null && HttpContext.Current.Request.Url != null)
            {
                if (HttpContext.Current.Request.Url.IsDefaultPort)
                {
                    return HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Host;
                }
                else
                {
                    return HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Authority;
                }
            }
            return "";
        }

        protected string GetCurrentUrl()
        {
            if (HttpContext.Current != null && HttpContext.Current.Request != null)
            {
                return HttpContext.Current.Request.Url.AbsoluteUri;
            }
            return "";
        }

        protected string GetUserIpAddress()
        {
            if (HttpContext.Current != null && HttpContext.Current.Request != null && HttpContext.Current.Request.UserHostAddress != null)
            {
                return HttpContext.Current.Request.UserHostAddress;
            }
            else
            {
                return "";
            }
        }

        #endregion
        
    }
}
